package com.jakewharton.mosaic.animation

import androidx.annotation.FloatRange

/**
 * [DecayAnimationSpec] stores the specification of an animation, including 1) the data type to be
 * animated, and 2) the animation configuration (i.e. [com.jakewharton.mosaic.animation.VectorizedDecayAnimationSpec]) that will be
 * used once the data (of type [T]) has been converted to [AnimationVector].
 *
 * Any type [T] can be animated by the system as long as a [com.jakewharton.mosaic.animation.TwoWayConverter] is supplied to convert
 * the data type [T] from and to an [AnimationVector]. There are a number of converters available
 * out of the box. For example, to animate [IntOffset] the system uses
 * [IntOffset.VectorConverter][IntOffset.Companion.VectorConverter] to convert the object to
 * [AnimationVector2D], so that both x and y dimensions are animated independently with separate
 * velocity tracking. This enables multidimensional objects to be animated in a true
 * multi-dimensional way. It is particularly useful for smoothly handling animation interruptions
 * (such as when the target changes during the animation).
 */
public interface DecayAnimationSpec<T> {

	/**
	 * Creates a [com.jakewharton.mosaic.animation.VectorizedDecayAnimationSpec] with the given [com.jakewharton.mosaic.animation.TwoWayConverter].
	 *
	 * The underlying animation system operates on [AnimationVector]s. [T] will be converted to
	 * [AnimationVector] to animate. [com.jakewharton.mosaic.animation.VectorizedDecayAnimationSpec] describes how the converted
	 * [AnimationVector] should be animated.
	 *
	 * @param typeConverter converts the type [T] from and to [AnimationVector] type
	 */
	public fun <V : AnimationVector> vectorize(
		typeConverter: TwoWayConverter<T, V>,
	): VectorizedDecayAnimationSpec<V>
}

/**
 * Calculates the target value of a decay animation based on the [initialValue] and
 * [initialVelocity], and the [typeConverter] that converts the given type [T] to [AnimationVector].
 *
 * @return target value where the animation will come to a natural stop
 */
public fun <T, V : AnimationVector> DecayAnimationSpec<T>.calculateTargetValue(
	typeConverter: TwoWayConverter<T, V>,
	initialValue: T,
	initialVelocity: T,
): T {
	val vectorizedSpec = vectorize(typeConverter)
	val targetVector =
		vectorizedSpec.getTargetValue(
			typeConverter.convertToVector(initialValue),
			typeConverter.convertToVector(initialVelocity),
		)
	return typeConverter.convertFromVector(targetVector)
}

/**
 * Calculates the target value of a Float decay animation based on the [initialValue] and
 * [initialVelocity].
 *
 * @return target value where the animation will come to a natural stop
 */
public fun DecayAnimationSpec<Float>.calculateTargetValue(
	initialValue: Float,
	initialVelocity: Float,
): Float {
	val vectorizedSpec = vectorize(Float.VectorConverter)
	val targetVector =
		vectorizedSpec.getTargetValue(
			AnimationVector(initialValue),
			AnimationVector(initialVelocity),
		)
	return targetVector.value
}

/**
 * Creates a decay animation spec where the friction/deceleration is always proportional to the
 * velocity. As a result, the velocity goes under an exponential decay. The constructor parameter,
 * [frictionMultiplier], can be tuned to adjust the amount of friction applied in the decay. The
 * higher the multiplier, the higher the friction, the sooner the animation will stop, and the
 * shorter distance the animation will travel with the same starting condition.
 * [absVelocityThreshold] describes the absolute value of a velocity threshold, below which the
 * animation is considered finished.
 *
 * @param frictionMultiplier The decay friction multiplier. This must be greater than `0`.
 * @param absVelocityThreshold The minimum speed, below which the animation is considered finished.
 *   Must be greater than `0`.
 */
public fun <T> exponentialDecay(
	@FloatRange(from = 0.0, fromInclusive = false) frictionMultiplier: Float = 1f,
	@FloatRange(from = 0.0, fromInclusive = false) absVelocityThreshold: Float = 0.1f,
): DecayAnimationSpec<T> = FloatExponentialDecaySpec(frictionMultiplier, absVelocityThreshold).generateDecayAnimationSpec()

/**
 * Creates a [DecayAnimationSpec] from a [com.jakewharton.mosaic.animation.FloatDecayAnimationSpec] by applying the given
 * [com.jakewharton.mosaic.animation.FloatDecayAnimationSpec] on every dimension of the [AnimationVector] that [T] converts to.
 */
public fun <T> FloatDecayAnimationSpec.generateDecayAnimationSpec(): DecayAnimationSpec<T> {
	return DecayAnimationSpecImpl(this)
}

private class DecayAnimationSpecImpl<T>(private val floatDecaySpec: FloatDecayAnimationSpec) : DecayAnimationSpec<T> {
	override fun <V : AnimationVector> vectorize(
		typeConverter: TwoWayConverter<T, V>,
	): VectorizedDecayAnimationSpec<V> = VectorizedFloatDecaySpec(floatDecaySpec)
}

private class VectorizedFloatDecaySpec<V : AnimationVector>(
	val floatDecaySpec: FloatDecayAnimationSpec,
) : VectorizedDecayAnimationSpec<V> {
	private lateinit var valueVector: V
	private lateinit var velocityVector: V
	private lateinit var targetVector: V
	override val absVelocityThreshold: Float = floatDecaySpec.absVelocityThreshold

	override fun getValueFromNanos(playTimeNanos: Long, initialValue: V, initialVelocity: V): V {
		if (!::valueVector.isInitialized) {
			valueVector = initialValue.newInstance()
		}
		for (i in 0 until valueVector.size) {
			valueVector[i] =
				floatDecaySpec.getValueFromNanos(playTimeNanos, initialValue[i], initialVelocity[i])
		}
		return valueVector
	}

	override fun getDurationNanos(initialValue: V, initialVelocity: V): Long {
		var maxDuration = 0L
		if (!::velocityVector.isInitialized) {
			velocityVector = initialValue.newInstance()
		}
		for (i in 0 until velocityVector.size) {
			maxDuration =
				maxOf(
					maxDuration,
					floatDecaySpec.getDurationNanos(initialValue[i], initialVelocity[i]),
				)
		}
		return maxDuration
	}

	override fun getVelocityFromNanos(playTimeNanos: Long, initialValue: V, initialVelocity: V): V {
		if (!::velocityVector.isInitialized) {
			velocityVector = initialValue.newInstance()
		}
		for (i in 0 until velocityVector.size) {
			velocityVector[i] =
				floatDecaySpec.getVelocityFromNanos(
					playTimeNanos,
					initialValue[i],
					initialVelocity[i],
				)
		}
		return velocityVector
	}

	override fun getTargetValue(initialValue: V, initialVelocity: V): V {
		if (!::targetVector.isInitialized) {
			targetVector = initialValue.newInstance()
		}
		for (i in 0 until targetVector.size) {
			targetVector[i] = floatDecaySpec.getTargetValue(initialValue[i], initialVelocity[i])
		}
		return targetVector
	}
}
